package com.nulldev.util.web.YouTubeExtractor.frontpage;

import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.getYTString;

import java.time.Duration;
import java.util.List;

import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.internal.backport.concurrency9.Lists;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor;

public class YTFrontpageExtractor {

	private static final Logger logger = LoggerFactory.getLogger();

	public static class Frontpage {
		private final List<FrontpageVideoEntry> videos;
		private final List<FrontpageSection> sections;
		private boolean wasNudged;
		private String continuation;

		public Frontpage() {
			this.videos = Lists.of();
			this.sections = Lists.of();
		}

		public Frontpage(final List<FrontpageVideoEntry> videos, final List<FrontpageSection> sections) {
			this.videos = videos;
			this.sections = sections;
		}

		public List<FrontpageVideoEntry> videos() {
			return this.videos;
		}

		public List<FrontpageSection> sections() {
			return this.sections;
		}

		protected Frontpage setContinuation(final String continuation) {
			this.continuation = continuation;
			return this;
		}

		protected Frontpage setNudging(final boolean state) {
			this.wasNudged = state;
			return this;
		}

		public boolean hasContinuation() {
			return this.continuation != null && !this.continuation.isEmpty();
		}

		public boolean wasNudged() {
			return this.wasNudged;
		}

		public String getContinuation() {
			return this.continuation;
		}
	}

	public static class FrontpageSection {
		private final String sectionTitle;
		private final List<FrontpageVideoEntry> sectionEntries;

		public FrontpageSection(final String sectionTitle, final List<FrontpageVideoEntry> sectionEntries) {
			this.sectionTitle = sectionTitle;
			this.sectionEntries = sectionEntries;
		}

		public String getSectionTitle() {
			return this.sectionTitle;
		}

		public List<FrontpageVideoEntry> videos() {
			return this.sectionEntries;
		}
	}

	public static class FrontpageVideoEntry {
		private final String videoID;
		private final String title;
		private final String author;
		private final long duration;
		private final long startTime;

		public FrontpageVideoEntry(final String videoID, final String title, final String author, final long duration, final long startTime) {
			this.videoID = videoID;
			this.title = title;
			this.author = author;
			this.duration = duration;
			this.startTime = startTime;
		}

		public String getVideoID() {
			return this.videoID;
		}

		public String getTitle() {
			return this.title;
		}

		public String getAuthor() {
			return this.author;
		}

		public long getDuration() {
			return this.duration;
		}

		public boolean isLive() {
			return this.duration == -2;
		}

		public boolean isUpcoming() {
			return this.duration == -3;
		}

		public boolean isShorts() {
			return this.duration == -4;
		}

		public boolean hasStartTime() {
			return this.startTime != -1;
		}

		public long getStartTime() {
			return this.startTime;
		}
	}

	public static Frontpage get() throws Exception {
		return get(true);
	}

	public static Frontpage get(final boolean sendConsentCookie) throws Exception {
		final String result = HttpClient.getString().url("https://www.youtube.com/").timeout(Duration.ofSeconds(15))
				.headers(sendConsentCookie ? YouTubeExtractor.DEFAULT_COOKIES2 : YouTubeExtractor.BLANK_COOKIES).headers(YouTubeExtractor.makeHeaders())
				.create().dataThenClose();
		final Executable<UniJsonValue> ytInitialData = ExecutorManager.globalSubmitNoAccel(() -> {
			try {
				final String ss = StringsUtil.substringBetweenO(result, "window[\"ytInitialData\"] = {", "};");
				if (ss == null) {
					final String ss1 = StringsUtil.substringBetweenO(result, ">var ytInitialData = {", "};");
					if (ss1 == null) {
						logger.error("Failed to fetch ytInitialData!");
						logger.debug(result);
						return null;
					}
					return UniJsonValue.parse("{" + ss1 + "}", true);
				}
				return UniJsonValue.parse("{" + ss + "}", true);
			} catch (Exception e) {
				logger.error("Failed to parse ytInitialData!", e);
				return null;
			}
		});
		final UniJsonValue core = ytInitialData.waitTillCompletion();
		if (core == null || core.isNull()) {
			logger.error("Failed to get ytInitialData!");
			return new Frontpage();
		}
		final UniJsonValue contents = core.get("contents");
		if (!contents.contains("twoColumnBrowseResultsRenderer")) {
			logger.error("twoColumnBrowseResultsRenderer not found!");
			return new Frontpage();
		}
		final UniJsonValue resultRenderer = contents.get("twoColumnBrowseResultsRenderer");
		final UniJsonValue tabs = resultRenderer.get("tabs");
		if (tabs == null || tabs.isNull() || !tabs.isArray())
			return new Frontpage();
		final UniJsonValue tabRenderer = tabs.first().get("tabRenderer");
		if (tabRenderer == null || tabRenderer.isNull()) {
			logger.error("tabRenderer not found!");
			logger.debug(tabs);
			return new Frontpage();
		}
		if (tabRenderer.contains("tabIdentifier") && !tabRenderer.getString("tabIdentifier").equalsIgnoreCase("FEwhat_to_watch")) {
			logger.warn("Unknown tab identifier: " + tabRenderer.getString("tabIdentifier"));
		}
		final UniJsonValue content = tabRenderer.get("content");
		final List<FrontpageVideoEntry> FVEs = FastLists.list(25);
		final List<FrontpageSection> sections = FastLists.list();
		/*
		 * YouTube apparently hates fresh cookies and will REFUSE to serve a frontpage.
		 * Cool.
		 */
		boolean was_nudged = false;
		String continuation = null;
		if (content.contains("richGridRenderer")) {
			final UniJsonValue richGridRenderer = content.get("richGridRenderer");
			final UniJsonValue fpContents = richGridRenderer.get("contents");
			if (!fpContents.isArray()) {
				logger.error("fpContents is not an array!");
				return new Frontpage();
			}
			for (final UniJsonValue fpItem : fpContents.asArray()) {
				if (fpItem.contains("richItemRenderer")) {
					final UniJsonValue fpc = fpItem.get("richItemRenderer").get("content");
					if (fpc == null || fpc.isNull()) {
						logger.error("Unknown fpItem content: " + fpItem);
						continue;
					}
					if (fpc.contains("videoRenderer")) {
						final UniJsonValue video = fpc.get("videoRenderer");
						String title = null;
						try {
							title = getYTString(video.get("title"));
						} catch (Exception e) {
							logger.error("Failed to get title!");
							logger.debug(video);
						}
						String author = null;
						try {
							author = getYTString(video.get("ownerText"));
						} catch (Exception e) {
							logger.error("Failed to get author!");
							logger.debug(author);
						}
						String time = "0:00";
						try {
							time = getYTString(video.get("lengthText"));
						} catch (Exception e) {
							if (logger.isDebugEnabled())
								logger.error("Failed to get time. Data: " + video.toString(), e);
						}
						long startTime = -1L;
						boolean isLive = false;
						boolean isUpcoming = false;
						try {
							if (video.get("thumbnailOverlays").first().get("thumbnailOverlayTimeStatusRenderer").getString("style").equalsIgnoreCase("LIVE"))
								isLive = true;
						} catch (Exception e) {
						}
						try {
							if (video.contains("upcomingEventData")) {
								isUpcoming = true;
								startTime = MathUtil.safeParseL(video.get("upcomingEventData").getString("startTime", "-1"), -1L);
							}
						} catch (Exception e) {
							logger.error("Failed to check for upcoming video!", e);
						}
						FVEs.add(new FrontpageVideoEntry(video.getString("videoId"), title, author,
								isUpcoming ? -3 : isLive ? -2 : MathUtil.TimeToSeconds.parseTime(time), startTime));
					} else
						logger.warn("Unknown fpc: " + fpc);
				} else if (fpItem.contains("richSectionRenderer")) {
					final UniJsonValue fpc = fpItem.get("richSectionRenderer").get("content");
					if (fpc.contains("compactPromotedItemRenderer")) {
						continue;
					} else if (fpc.contains("feedNudgeRenderer")) {
						was_nudged = true;
						continue;
					} else if (fpc.contains("richShelfRenderer")) {
						final UniJsonValue rsr = fpc.get("richShelfRenderer");
						final UniJsonValue _contents = rsr.get("contents");
						final String sectionTitle = rsr.get("title").get("runs").first().getString("text");
						final List<FrontpageVideoEntry> sectionEntries = FastLists.list();
						for (final UniJsonValue shelfItem : _contents.asArray()) {
							if (shelfItem.contains("richItemRenderer")) {
								final UniJsonValue richContent = shelfItem.get("richItemRenderer").get("content");
								if (richContent.contains("videoRenderer")) {
									final UniJsonValue video = richContent.get("videoRenderer");
									String title = null;
									try {
										title = getYTString(video.get("title"));
									} catch (Exception e) {
										logger.error("Failed to get title!");
										logger.debug(video);
									}
									String author = null;
									try {
										author = getYTString(video.get("ownerText"));
									} catch (Exception e) {
										logger.error("Failed to get author!");
										logger.debug(author);
									}
									String time = "0:00";
									try {
										time = getYTString(video.get("lengthText"));
									} catch (Exception e) {
										if (logger.isDebugEnabled())
											logger.error("Failed to get time. Data: " + video.toString(), e);
									}
									long startTime = -1L;
									boolean isLive = false;
									boolean isUpcoming = false;
									try {
										if (video.get("thumbnailOverlays").first().get("thumbnailOverlayTimeStatusRenderer").getString("style")
												.equalsIgnoreCase("LIVE"))
											isLive = true;
									} catch (Exception e) {
									}
									try {
										if (video.contains("upcomingEventData")) {
											isUpcoming = true;
											startTime = MathUtil.safeParseL(video.get("upcomingEventData").getString("startTime", "-1"), -1L);
										}
									} catch (Exception e) {
										logger.error("Failed to check for upcoming video!", e);
									}
									sectionEntries.add(new FrontpageVideoEntry(video.getString("videoId"), title, author,
											isUpcoming ? -3 : isLive ? -2 : MathUtil.TimeToSeconds.parseTime(time), startTime));
								} else if (richContent.contains("reelItemRenderer")) {
									final UniJsonValue reelItemRenderer = richContent.get("reelItemRenderer");
									final String videoId = reelItemRenderer.getString("videoId");
									final String title = getYTString(reelItemRenderer.get("headline"));
									final String author = "SHORTS";
									sectionEntries.add(new FrontpageVideoEntry(videoId, title, author, -4, -1L));
								} else
									logger.warn("Unknown rich item: " + richContent);
							} else
								logger.warn("Unknown shelf item: " + shelfItem);
						}
						sections.add(new FrontpageSection(sectionTitle, sectionEntries));
					} else {
						logger.warn("Unknown rich section: " + fpc);
					}
				} else if (fpItem.contains("continuationItemRenderer")) {
					final UniJsonValue fpc = fpItem.get("continuationItemRenderer");
					continuation = fpc.get("continuationEndpoint").get("continuationCommand").getString("token");
				} else {
					logger.warn("Unknown fpItem: " + fpItem);
				}
			}
			return new Frontpage(FVEs, sections).setContinuation(continuation).setNudging(was_nudged);
		} else {
			logger.error("Unknown contents of 'content'! Bailing out.");
			logger.debug(content);
			return new Frontpage();
		}
	}

	public static void main(String[] args) throws Exception {
		get();
	}
}
